/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package fr.inria.openmath.omapi.implementation;

/**
 *
 * @author Mr.Sam
 */
import java.io.*;
import fr.inria.openmath.omapi.*;
import java.math.BigInteger;
import java.util.Hashtable;
import java.util.Enumeration;

public class BinaryPrinter implements StreamPrinter {

    DataOutputStream output;

    Sharetable stringSharetable;

    Sharetable symbolSharetable;

    Sharetable variableSharetable;

    private class Sharetable extends Hashtable {

        int shareId;

        public Sharetable (int initialCapacity) {
            super(initialCapacity);
        }

        public String toString() {
            String s = "";
            Object k;
            for (Enumeration e = keys(); e.hasMoreElements();) {
                k = e.nextElement();
                s += k + " -> " + get(k) + "\n";
            }
            return s;
        }

    }

    private class Share {

        Object o;

        int id;

        public Share (Object o, int id) {
            this.o = o;
            this.id = id;
        }

        public int hashCode() {
            return o.hashCode();
        }

        public boolean equals(Share s) {
            return o.equals(s.o) && (id == s.id);
        }

        public String toString() {
            return id + ":" + o.toString();
        }

    }

    private void resetSharetable(Sharetable st) {
        if (st.shareId > 0) {
            st.clear();
            st.shareId = 0;
        }
    }


    private int shareIfNeeded(Sharetable st, Object o) {
        Share s;
        if ((s = (Share) st.get(o)) == null) {
            if (st.shareId > 255) {
                // 256 first objects already shared
                // don't add new entries for sharing
                System.out.println("0 no room left: " + o);
                return -1;
            }
            //            System.out.println("0 " + st.shareId + " store: " + o);
            st.put(o, new Share(o, st.shareId));
            st.shareId++;
            return -1;
        }
        //        System.out.println(s.id + " found: " + o);
        return s.id;
    }

    public BinaryPrinter (OutputStream output) {
        if (!(output.getClass()).isAssignableFrom(
                (new DataOutputStream(output)).getClass())) {
            throw new RuntimeException(
                    this.getClass().getName() +
                    "[constructor]: invalid call, output must be a DataOutputStream or assignable. (found "
                    + output.getClass().getName() + ")");
        }
        stringSharetable = new Sharetable(512);
        symbolSharetable = new Sharetable(512);
        variableSharetable = new Sharetable(512);
        this.output = (DataOutputStream) output;
    }

    public BinaryPrinter () {
        this(new BufferedOutputStream(new DataOutputStream(System.out)));
    }

    /**
     * @see fr.inria.openmath.omapi.StreamPrinter#setOutput
     */
    public void setOutput(OutputStream output) throws OMException {
        if (!(output.getClass()).isAssignableFrom(
                (new DataOutputStream(output)).getClass())) {
            throw new RuntimeException(
                    this.getClass().getName() +
                    "[setOutput]: invalid call, output must be a DataOutputStream or assignable. (found "
                    + output.getClass().getName() + ")");
        }
        this.output = (DataOutputStream) output;
    }

    /**
     * @see fr.inria.openmath.omapi.StreamPrinter#getOutput
     */
    public OutputStream getOutput() {
        return this.output;
    }

    private byte tagBits(byte tag, int len) {
        if (len > 255)
            return (byte) (tag | Node.OM_BE_BIG_LEN_MASK);
        return tag;
    }

    private byte shareBits(byte tag) {
        return (byte) (tag | Node.OM_BE_SHARED_MASK);
    }

    private void putLength(int len) throws IOException {
        if (len < 256) {
            output.writeByte(len);
        } else {
            output.writeInt(len);
        }
    }

    private boolean isWCString(String s) {
        int l, i;
        l = s.length();
        for (i = 0; i < l; i++) {
            if ((s.charAt(i) & 0xff00) != 0)
                return true;
        }
        return false;
    }


    /**
     * @see fr.inria.openmath.omapi.Printer#putBeginObject
     */
    public void putBeginObject() throws IOException {
        output.writeByte(Node.OM_OBJECT);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putEndObject
     */
    public void putEndObject() throws IOException {
        output.writeByte(Node.OM_END_OBJECT);
        output.flush();
        resetSharetable(stringSharetable);
        resetSharetable(variableSharetable);
        resetSharetable(symbolSharetable);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putBeginApplication
     */
    public void putBeginApplication() throws IOException {
        output.writeByte(Node.OM_APP);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putEndApplication
     */
    public void putEndApplication() throws IOException {
        output.writeByte(Node.OM_END_APP);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putBeginAttribution
     */
    public void putBeginAttribution() throws IOException {
        output.writeByte(Node.OM_ATTR);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putEndAttribution
     */
    public void putEndAttribution() throws IOException {
        output.writeByte(Node.OM_END_ATTR);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putBeginAttributePairs
     */
    public void putBeginAttributePairs() throws IOException {
        output.writeByte(Node.OM_ATP);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putEndAttributePairs
     */
    public void putEndAttributePairs() throws IOException {
        output.writeByte(Node.OM_END_ATP);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putBeginBind
     */
    public void putBeginBind() throws IOException {
        output.writeByte(Node.OM_BIND);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putEndBind
     */
    public void putEndBind() throws IOException {
        output.writeByte(Node.OM_END_BIND);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putBeginBVars
     */
    public void putBeginBVars() throws IOException {
        output.writeByte(Node.OM_BVAR);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putEndBVars
     */
    public void putEndBVars() throws IOException {
        output.writeByte(Node.OM_END_BVAR);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putBeginError
     */
    public void putBeginError() throws IOException {
        output.writeByte(Node.OM_ERROR);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putEndError
     */
    public void putEndError() throws IOException {
        output.writeByte(Node.OM_END_ERROR);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putInteger
     */
    public void putInteger(int value) throws IOException {
        if ((value <= 127) && (value >= -128)) {
            // fits in a byte
            output.writeByte(tagBits(Node.OM_INT32, 0));
            output.writeByte((byte) value);
        } else {
            // need more than one byte
            output.writeByte(tagBits(Node.OM_INT32, 256));
            output.writeInt(value);
        }
    }

    /**
     * @param base is unused in binary encoding.
     * @see fr.inria.openmath.omapi.Printer#putInteger
     */
    public void putInteger(int value, int base) throws IOException {
        putInteger(value);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putInteger
     */
    public void putInteger(BigInteger value, int base) throws IOException {
        String digits;
        boolean negSign = false;
        int len;

        if (base != 16)
            base = 10;                 // OM spec says hex or dec, thats all!
        digits = value.toString(base);
        if (digits.charAt(0) == '-') {
            negSign = true;
            if (digits.charAt(1) == 'x')
                digits = digits.substring(2); // OPTIM :(
            else
                digits = digits.substring(1); // OPTIM :(
        }
        len = digits.length();
        output.writeByte(tagBits(Node.OM_BIGINT, len));
        putLength(len);
        if (negSign) {
            switch (base) {
                case 16: 
                    output.writeByte(Node.OM_BE_NEG_BASE_16);
                    break;
                default: 
                    output.writeByte(Node.OM_BE_NEG_BASE_10);
                    break;
            }
        } else {
            switch (base) {
                case 16: 
                    output.writeByte(Node.OM_BE_POS_BASE_16);
                    break;
                default: 
                    output.writeByte(Node.OM_BE_POS_BASE_10);
                    break;
            }
        }
        output.writeBytes(digits);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putInteger
     */
    public void putInteger(BigInteger value) throws IOException {
        putInteger(value, 10);
    }

    /**
     * @param base base is useless with binary encoding.
     * @see fr.inria.openmath.omapi.Printer#putFloat
     */
    public void putFloat(double value, int base) throws IOException {
        long l;
        l = java.lang.Double.doubleToLongBits(value);
        l = (((l & 0xff00000000000000L) >>> 56) |
                ((l & 0x00ff000000000000L) >>> 40)
                | ((l & 0x0000ff0000000000L) >>> 24)
                | ((l & 0x000000ff00000000L) >>> 8)
                | ((l & 0x00000000ff000000L) << 8)
                | ((l & 0x0000000000ff0000L) << 24)
                | ((l & 0x000000000000ff00L) << 40)
                | ((l & 0x00000000000000ffL) << 56));
        output.writeByte(Node.OM_FLOAT64);
        output.writeLong(l);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putFloat
     */
    public void putFloat(double value) throws IOException {
        output.writeByte(Node.OM_FLOAT64);
        output.writeDouble(value);
    }


    /**
     * @see fr.inria.openmath.omapi.Printer#putString
     */
    public void putString(String value) throws IOException {
        int id, len;

        len = value.length();
        if ((len < 256) && ((id =
                shareIfNeeded(stringSharetable, value)) >= 0)) {
            // shared object, output only its tag and its id.
            output.writeByte(shareBits(Node.OM_WCSTRING));
            output.writeByte((byte) id);
        } else {
            // non shared object, normal output
            if (isWCString(value)) {
                output.writeByte(tagBits(Node.OM_WCSTRING, len));
                putLength(value.length() * 2);
                output.writeChars(value);
            } else {
                output.writeByte(Node.OM_STRING);
                putLength(len);
                output.writeBytes(value);
            }
        }
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putVariable
     */
    public void putVariable(String name) throws IOException {
        int id, len;

        if ((id = shareIfNeeded(variableSharetable, name)) >= 0) {
            // shared object, output only its tag and its id.
            output.writeByte(shareBits(Node.OM_VAR));
            output.writeByte((byte) id);
        } else {
            len = name.length();
            output.writeByte(tagBits(Node.OM_VAR, len));
            putLength(len);
            output.writeBytes(name);
        }
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putByteArray
     */
    public void putByteArray(byte[] value, int len) throws IOException {
        output.writeByte(tagBits(Node.OM_BYTEARRAY, len));
        putLength(len);
        for (int i = 0; i < len; i++) {
            output.writeByte(value[i]);
        }
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putByteArray
     */
    public void putByteArray(byte[] value) throws IOException {
        int len = value.length;
        output.writeByte(tagBits(Node.OM_BYTEARRAY, len));
        putLength(len);
        for (int i = 0; i < len; i++) {
            output.writeByte(value[i]);
        }
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putSymbol
     */
    public void putSymbol(Symbol symbol) throws IOException {
        int cdl, nml, id;

        if ((id = shareIfNeeded(symbolSharetable, symbol)) >= 0) {
            // shared object, output only its tag and its id.
            output.writeByte(shareBits(Node.OM_SYMBOL));
            output.writeByte((byte) id);
        } else {
            cdl = symbol.getCD().length();
            nml = symbol.getName().length();
            output.writeByte(tagBits(Node.OM_SYMBOL, java.lang.Math.max(cdl,
                    nml)));
            putLength(cdl);
            putLength(nml);
            output.writeBytes(symbol.getCD());
            output.writeBytes(symbol.getName());
        }
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putComment
     */
    public void putComment(String comment) throws IOException {
        output.writeByte(tagBits(Node.OM_COMMENT, comment.length()));
        putLength(comment.length());
        output.writeBytes(comment);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#putPInstruction
     */
    public void putPInstruction(String pi) throws IOException {
        output.writeByte(tagBits(Node.OM_PINSTRUCTION, pi.length()));
        putLength(pi.length());
        output.writeBytes(pi);
    }

    /**
     * @see fr.inria.openmath.omapi.Printer#endPrint
     */
    public void endPrint() throws IOException {
        output.flush();
    }


}
